"""
Dialogue Session Manager for Cross-Temporal Conversations.

This module manages multi-participant conversations, historical context preservation,
learning objective tracking, and conversation analytics.
"""

import numpy as np
import logging
import asyncio
import time
import uuid
from typing import Dict, List, Optional, Tuple, Any, Set, Union
from dataclasses import dataclass, field
from enum import Enum
from collections import defaultdict, deque
import json
from datetime import datetime

from ..personality.models import PersonalityVector
from ..memory.manager import MemoryManager
from .generator import CrossTemporalDialogueGenerator, DialogueContext

logger = logging.getLogger(__name__)


class SessionStatus(Enum):
    """Session status states."""
    ACTIVE = "active"
    PAUSED = "paused"
    COMPLETED = "completed"
    ARCHIVED = "archived"
    ERROR = "error"


class MessageType(Enum):
    """Types of messages in dialogue sessions."""
    DIALOGUE = "dialogue"
    SYSTEM = "system"
    ANNOTATION = "annotation"
    FEEDBACK = "feedback"
    CORRECTION = "correction"


@dataclass
class Participant:
    """Participant in a dialogue session."""
    participant_id: str
    name: str
    personality_vector: PersonalityVector
    role: str = "speaker"
    historical_period: str = ""
    cultural_background: str = ""
    expertise_areas: List[str] = field(default_factory=list)
    speaking_style: str = "default"
    is_active: bool = True
    last_activity: Optional[datetime] = None


@dataclass
class Message:
    """Message in a dialogue session."""
    message_id: str
    session_id: str
    participant_id: str
    content: str
    message_type: MessageType
    timestamp: datetime
    response_to: Optional[str] = None  # Reply to message_id
    accuracy_score: float = 0.0
    cultural_authenticity: float = 0.0
    confidence_score: float = 0.0
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class LearningObjective:
    """Learning objective for educational dialogue sessions."""
    objective_id: str
    description: str
    target_concepts: List[str]
    assessment_criteria: List[str]
    progress_indicators: List[str]
    completion_threshold: float = 0.8
    current_progress: float = 0.0
    is_achieved: bool = False


@dataclass
class SessionAnalytics:
    """Analytics data for a dialogue session."""
    total_messages: int = 0
    messages_by_participant: Dict[str, int] = field(default_factory=lambda: defaultdict(int))
    average_accuracy_score: float = 0.0
    average_cultural_authenticity: float = 0.0
    anachronisms_detected: int = 0
    corrections_applied: int = 0
    session_duration_minutes: float = 0.0
    engagement_score: float = 0.0
    learning_progress: Dict[str, float] = field(default_factory=dict)
    topic_coverage: List[str] = field(default_factory=list)


class DialogueSession:
    """
    Manages a multi-participant cross-temporal dialogue session.
    
    Features:
    - Multi-participant conversation management
    - Historical context preservation across sessions
    - Learning objective tracking for educational use
    - Conversation history and analytics
    """
    
    def __init__(
        self,
        session_id: str,
        context: DialogueContext,
        participants: List[Participant],
        learning_objectives: Optional[List[LearningObjective]] = None,
        max_messages: int = 1000,
        session_timeout_minutes: int = 120
    ):
        """
        Initialize dialogue session.
        
        Args:
            session_id: Unique session identifier
            context: Dialogue context
            participants: List of session participants
            learning_objectives: Optional learning objectives
            max_messages: Maximum messages per session
            session_timeout_minutes: Session timeout in minutes
        """
        self.session_id = session_id
        self.context = context
        self.participants = {p.participant_id: p for p in participants}
        self.learning_objectives = {
            obj.objective_id: obj for obj in (learning_objectives or [])
        }
        self.max_messages = max_messages
        self.session_timeout_minutes = session_timeout_minutes
        
        # Session state
        self.status = SessionStatus.ACTIVE
        self.created_at = datetime.now()
        self.last_activity = datetime.now()
        self.completed_at = None
        
        # Message storage
        self.messages = deque(maxlen=max_messages)
        self.message_index = {}  # message_id -> Message
        
        # Turn management
        self.current_speaker = None
        self.speaking_order = list(self.participants.keys())
        self.turn_index = 0
        
        # Analytics
        self.analytics = SessionAnalytics()
        
        # Context memory for coherence
        self.context_memory = deque(maxlen=50)  # Last 50 exchanges for context
        
        logger.info(f"Created dialogue session {session_id} with {len(participants)} participants")
    
    def add_message(
        self,
        participant_id: str,
        content: str,
        message_type: MessageType = MessageType.DIALOGUE,
        response_to: Optional[str] = None,
        accuracy_score: float = 0.0,
        cultural_authenticity: float = 0.0,
        confidence_score: float = 0.0,
        metadata: Optional[Dict[str, Any]] = None
    ) -> Message:
        """
        Add a message to the session.
        
        Args:
            participant_id: ID of message sender
            content: Message content
            message_type: Type of message
            response_to: Optional message ID being replied to
            accuracy_score: Historical accuracy score
            cultural_authenticity: Cultural authenticity score
            confidence_score: Generation confidence score
            metadata: Additional message metadata
            
        Returns:
            Created message object
        """
        if self.status != SessionStatus.ACTIVE:
            raise ValueError(f"Cannot add messages to {self.status.value} session")
        
        if participant_id not in self.participants:
            raise ValueError(f"Unknown participant: {participant_id}")
        
        # Create message
        message = Message(
            message_id=str(uuid.uuid4()),
            session_id=self.session_id,
            participant_id=participant_id,
            content=content,
            message_type=message_type,
            timestamp=datetime.now(),
            response_to=response_to,
            accuracy_score=accuracy_score,
            cultural_authenticity=cultural_authenticity,
            confidence_score=confidence_score,
            metadata=metadata or {}
        )
        
        # Add to storage
        self.messages.append(message)
        self.message_index[message.message_id] = message
        
        # Update participant activity
        if participant_id in self.participants:
            self.participants[participant_id].last_activity = datetime.now()
        
        # Update session activity
        self.last_activity = datetime.now()
        
        # Update context memory
        if message_type == MessageType.DIALOGUE:
            self.context_memory.append({
                'participant_id': participant_id,
                'content': content,
                'timestamp': message.timestamp.isoformat(),
                'accuracy_score': accuracy_score
            })
        
        # Update analytics
        self._update_analytics(message)
        
        # Check learning objectives progress
        if self.learning_objectives:
            self._update_learning_progress(message)
        
        logger.debug(f"Added message {message.message_id} from {participant_id}")
        
        return message
    
    def get_conversation_history(
        self,
        limit: Optional[int] = None,
        include_system_messages: bool = False
    ) -> List[Dict[str, str]]:
        """
        Get conversation history for context.
        
        Args:
            limit: Maximum number of messages to return
            include_system_messages: Whether to include system messages
            
        Returns:
            List of message dictionaries for LLM context
        """
        messages = list(self.messages)
        
        if not include_system_messages:
            messages = [m for m in messages if m.message_type == MessageType.DIALOGUE]
        
        if limit:
            messages = messages[-limit:]
        
        history = []
        for message in messages:
            participant = self.participants.get(message.participant_id)
            role = "assistant" if participant and participant.role == "speaker" else "user"
            
            history.append({
                "role": role,
                "content": message.content,
                "participant_id": message.participant_id,
                "timestamp": message.timestamp.isoformat()
            })
        
        return history
    
    def get_next_speaker(self) -> Optional[str]:
        """Get the next participant who should speak."""
        if not self.participants:
            return None
        
        # Simple round-robin approach
        active_participants = [
            p_id for p_id, p in self.participants.items() 
            if p.is_active
        ]
        
        if not active_participants:
            return None
        
        if self.current_speaker is None:
            return active_participants[0]
        
        try:
            current_index = active_participants.index(self.current_speaker)
            next_index = (current_index + 1) % len(active_participants)
            return active_participants[next_index]
        except ValueError:
            # Current speaker not in active list, return first active
            return active_participants[0]
    
    def set_current_speaker(self, participant_id: str):
        """Set the current speaker."""
        if participant_id in self.participants:
            self.current_speaker = participant_id
        else:
            logger.warning(f"Cannot set unknown participant {participant_id} as speaker")
    
    def pause_session(self):
        """Pause the session."""
        if self.status == SessionStatus.ACTIVE:
            self.status = SessionStatus.PAUSED
            logger.info(f"Session {self.session_id} paused")
    
    def resume_session(self):
        """Resume a paused session."""
        if self.status == SessionStatus.PAUSED:
            self.status = SessionStatus.ACTIVE
            self.last_activity = datetime.now()
            logger.info(f"Session {self.session_id} resumed")
    
    def complete_session(self):
        """Mark session as completed."""
        self.status = SessionStatus.COMPLETED
        self.completed_at = datetime.now()
        
        # Finalize analytics
        duration = (self.completed_at - self.created_at).total_seconds() / 60.0
        self.analytics.session_duration_minutes = duration
        
        logger.info(f"Session {self.session_id} completed after {duration:.1f} minutes")
    
    def is_expired(self) -> bool:
        """Check if session has expired due to inactivity."""
        if self.status != SessionStatus.ACTIVE:
            return False
        
        inactive_minutes = (datetime.now() - self.last_activity).total_seconds() / 60.0
        return inactive_minutes > self.session_timeout_minutes
    
    def add_participant(self, participant: Participant):
        """Add a new participant to the session."""
        self.participants[participant.participant_id] = participant
        logger.info(f"Added participant {participant.participant_id} to session {self.session_id}")
    
    def remove_participant(self, participant_id: str):
        """Remove a participant from the session."""
        if participant_id in self.participants:
            self.participants[participant_id].is_active = False
            logger.info(f"Removed participant {participant_id} from session {self.session_id}")
    
    def update_learning_objective(self, objective_id: str, progress: float):
        """Update progress on a learning objective."""
        if objective_id in self.learning_objectives:
            objective = self.learning_objectives[objective_id]
            objective.current_progress = min(1.0, max(0.0, progress))
            
            if objective.current_progress >= objective.completion_threshold:
                objective.is_achieved = True
                logger.info(f"Learning objective {objective_id} achieved")
            
            self.analytics.learning_progress[objective_id] = objective.current_progress
    
    def get_session_summary(self) -> Dict[str, Any]:
        """Get comprehensive session summary."""
        return {
            'session_id': self.session_id,
            'status': self.status.value,
            'created_at': self.created_at.isoformat(),
            'last_activity': self.last_activity.isoformat(),
            'completed_at': self.completed_at.isoformat() if self.completed_at else None,
            'context': {
                'historical_period': self.context.historical_period,
                'location': self.context.location,
                'topic': self.context.topic,
                'cultural_context': self.context.cultural_context
            },
            'participants': {
                p_id: {
                    'name': p.name,
                    'role': p.role,
                    'historical_period': p.historical_period,
                    'is_active': p.is_active,
                    'last_activity': p.last_activity.isoformat() if p.last_activity else None
                }
                for p_id, p in self.participants.items()
            },
            'learning_objectives': {
                obj_id: {
                    'description': obj.description,
                    'progress': obj.current_progress,
                    'achieved': obj.is_achieved
                }
                for obj_id, obj in self.learning_objectives.items()
            },
            'analytics': {
                'total_messages': self.analytics.total_messages,
                'messages_by_participant': dict(self.analytics.messages_by_participant),
                'average_accuracy_score': self.analytics.average_accuracy_score,
                'average_cultural_authenticity': self.analytics.average_cultural_authenticity,
                'anachronisms_detected': self.analytics.anachronisms_detected,
                'corrections_applied': self.analytics.corrections_applied,
                'session_duration_minutes': self.analytics.session_duration_minutes,
                'engagement_score': self.analytics.engagement_score,
                'learning_progress': dict(self.analytics.learning_progress)
            },
            'message_count': len(self.messages)
        }
    
    def export_conversation(self, format_type: str = "json") -> str:
        """Export conversation in specified format."""
        conversation_data = {
            'session_info': self.get_session_summary(),
            'messages': [
                {
                    'message_id': msg.message_id,
                    'participant_id': msg.participant_id,
                    'participant_name': self.participants[msg.participant_id].name,
                    'content': msg.content,
                    'message_type': msg.message_type.value,
                    'timestamp': msg.timestamp.isoformat(),
                    'accuracy_score': msg.accuracy_score,
                    'cultural_authenticity': msg.cultural_authenticity,
                    'confidence_score': msg.confidence_score,
                    'response_to': msg.response_to,
                    'metadata': msg.metadata
                }
                for msg in self.messages
            ]
        }
        
        if format_type == "json":
            return json.dumps(conversation_data, indent=2, ensure_ascii=False)
        elif format_type == "text":
            return self._export_as_text(conversation_data)
        else:
            raise ValueError(f"Unsupported export format: {format_type}")
    
    def _export_as_text(self, data: Dict) -> str:
        """Export conversation as readable text."""
        lines = []
        lines.append(f"Dialogue Session: {data['session_info']['session_id']}")
        lines.append(f"Historical Period: {data['session_info']['context']['historical_period']}")
        lines.append(f"Location: {data['session_info']['context']['location']}")
        lines.append(f"Topic: {data['session_info']['context']['topic']}")
        lines.append("")
        
        lines.append("Participants:")
        for p_id, p_info in data['session_info']['participants'].items():
            lines.append(f"  - {p_info['name']} ({p_info['historical_period']})")
        lines.append("")
        
        lines.append("Conversation:")
        for msg in data['messages']:
            if msg['message_type'] == 'dialogue':
                participant_name = msg['participant_name']
                timestamp = msg['timestamp'][:16]  # YYYY-MM-DD HH:MM
                lines.append(f"[{timestamp}] {participant_name}: {msg['content']}")
        
        return "\n".join(lines)
    
    def _update_analytics(self, message: Message):
        """Update session analytics with new message."""
        self.analytics.total_messages += 1
        self.analytics.messages_by_participant[message.participant_id] += 1
        
        # Update running averages for dialogue messages
        if message.message_type == MessageType.DIALOGUE:
            dialogue_count = sum(
                1 for m in self.messages 
                if m.message_type == MessageType.DIALOGUE
            )
            
            if dialogue_count > 0:
                # Update accuracy score
                current_avg = self.analytics.average_accuracy_score
                self.analytics.average_accuracy_score = (
                    (current_avg * (dialogue_count - 1) + message.accuracy_score) / 
                    dialogue_count
                )
                
                # Update cultural authenticity
                current_avg = self.analytics.average_cultural_authenticity
                self.analytics.average_cultural_authenticity = (
                    (current_avg * (dialogue_count - 1) + message.cultural_authenticity) / 
                    dialogue_count
                )
        
        # Track anachronisms and corrections from metadata
        if message.metadata:
            if message.metadata.get('anachronism_detected', False):
                self.analytics.anachronisms_detected += 1
            
            if message.metadata.get('corrections_applied', False):
                self.analytics.corrections_applied += 1
        
        # Update engagement score based on message frequency and quality
        self._calculate_engagement_score()
    
    def _update_learning_progress(self, message: Message):
        """Update learning objective progress based on message content."""
        for obj_id, objective in self.learning_objectives.items():
            # Simple keyword-based progress tracking
            content_lower = message.content.lower()
            concept_mentions = sum(
                1 for concept in objective.target_concepts
                if concept.lower() in content_lower
            )
            
            if concept_mentions > 0:
                # Increase progress slightly
                progress_increase = min(0.1, concept_mentions * 0.05)
                new_progress = min(1.0, objective.current_progress + progress_increase)
                self.update_learning_objective(obj_id, new_progress)
    
    def _calculate_engagement_score(self):
        """Calculate engagement score based on various factors."""
        if not self.messages:
            self.analytics.engagement_score = 0.0
            return
        
        # Factor 1: Message frequency (more frequent = higher engagement)
        duration_minutes = max(1.0, (datetime.now() - self.created_at).total_seconds() / 60.0)
        messages_per_minute = len(self.messages) / duration_minutes
        frequency_score = min(1.0, messages_per_minute / 2.0)  # Normalize to 2 messages/min = 1.0
        
        # Factor 2: Participant distribution (more balanced = higher engagement)
        participant_counts = list(self.analytics.messages_by_participant.values())
        if len(participant_counts) > 1:
            balance_score = 1.0 - (max(participant_counts) - min(participant_counts)) / max(participant_counts)
        else:
            balance_score = 1.0 if participant_counts else 0.0
        
        # Factor 3: Average message quality
        dialogue_messages = [m for m in self.messages if m.message_type == MessageType.DIALOGUE]
        if dialogue_messages:
            avg_confidence = sum(m.confidence_score for m in dialogue_messages) / len(dialogue_messages)
        else:
            avg_confidence = 0.0
        
        # Combined engagement score
        self.analytics.engagement_score = (
            frequency_score * 0.4 + 
            balance_score * 0.3 + 
            avg_confidence * 0.3
        )


class SessionManager:
    """
    Manager for multiple dialogue sessions.
    
    Features:
    - Session lifecycle management
    - Cross-session context preservation
    - Performance monitoring and analytics
    - Session archival and retrieval
    """
    
    def __init__(
        self,
        memory_manager: MemoryManager,
        dialogue_generator: CrossTemporalDialogueGenerator,
        max_active_sessions: int = 100
    ):
        """
        Initialize session manager.
        
        Args:
            memory_manager: Memory system for persistence
            dialogue_generator: Dialogue generation system
            max_active_sessions: Maximum concurrent active sessions
        """
        self.memory_manager = memory_manager
        self.dialogue_generator = dialogue_generator
        self.max_active_sessions = max_active_sessions
        
        # Session storage
        self.active_sessions = {}  # session_id -> DialogueSession
        self.archived_sessions = {}  # session_id -> session_summary
        
        # Performance tracking
        self.manager_stats = {
            'total_sessions_created': 0,
            'active_sessions_count': 0,
            'completed_sessions_count': 0,
            'expired_sessions_count': 0,
            'average_session_duration': 0.0
        }
        
        logger.info("SessionManager initialized")
    
    async def create_session(
        self,
        context: DialogueContext,
        participants: List[Participant],
        learning_objectives: Optional[List[LearningObjective]] = None,
        session_id: Optional[str] = None
    ) -> DialogueSession:
        """
        Create a new dialogue session.
        
        Args:
            context: Dialogue context
            participants: Session participants
            learning_objectives: Optional learning objectives
            session_id: Optional custom session ID
            
        Returns:
            Created dialogue session
        """
        # Check session limit
        if len(self.active_sessions) >= self.max_active_sessions:
            await self._cleanup_expired_sessions()
            
            if len(self.active_sessions) >= self.max_active_sessions:
                raise Exception("Maximum active sessions limit reached")
        
        # Generate session ID if not provided
        if session_id is None:
            session_id = str(uuid.uuid4())
        
        # Create session
        session = DialogueSession(
            session_id=session_id,
            context=context,
            participants=participants,
            learning_objectives=learning_objectives
        )
        
        # Store session
        self.active_sessions[session_id] = session
        
        # Update statistics
        self.manager_stats['total_sessions_created'] += 1
        self.manager_stats['active_sessions_count'] = len(self.active_sessions)
        
        logger.info(f"Created session {session_id}")
        return session
    
    def get_session(self, session_id: str) -> Optional[DialogueSession]:
        """Get active session by ID."""
        return self.active_sessions.get(session_id)
    
    async def end_session(self, session_id: str):
        """End a session and archive it."""
        if session_id not in self.active_sessions:
            logger.warning(f"Cannot end unknown session {session_id}")
            return
        
        session = self.active_sessions[session_id]
        session.complete_session()
        
        # Archive session
        await self._archive_session(session)
        
        # Remove from active sessions
        del self.active_sessions[session_id]
        
        # Update statistics
        self.manager_stats['active_sessions_count'] = len(self.active_sessions)
        self.manager_stats['completed_sessions_count'] += 1
        
        logger.info(f"Ended and archived session {session_id}")
    
    async def _archive_session(self, session: DialogueSession):
        """Archive a completed session."""
        summary = session.get_session_summary()
        self.archived_sessions[session.session_id] = summary
        
        # Optionally persist to memory system
        # This would save the session to long-term storage
        logger.debug(f"Archived session {session.session_id}")
    
    async def _cleanup_expired_sessions(self):
        """Clean up expired sessions."""
        expired_sessions = []
        
        for session_id, session in self.active_sessions.items():
            if session.is_expired():
                expired_sessions.append(session_id)
        
        for session_id in expired_sessions:
            session = self.active_sessions[session_id]
            session.status = SessionStatus.ARCHIVED
            
            # Archive expired session
            await self._archive_session(session)
            del self.active_sessions[session_id]
            
            self.manager_stats['expired_sessions_count'] += 1
            logger.info(f"Expired and archived session {session_id}")
        
        if expired_sessions:
            self.manager_stats['active_sessions_count'] = len(self.active_sessions)
    
    def get_active_sessions(self) -> List[str]:
        """Get list of active session IDs."""
        return list(self.active_sessions.keys())
    
    def get_session_summaries(self) -> Dict[str, Dict[str, Any]]:
        """Get summaries of all sessions (active and archived)."""
        summaries = {}
        
        # Active sessions
        for session_id, session in self.active_sessions.items():
            summaries[session_id] = session.get_session_summary()
        
        # Archived sessions
        summaries.update(self.archived_sessions)
        
        return summaries
    
    def get_performance_metrics(self) -> Dict[str, Any]:
        """Get session manager performance metrics."""
        metrics = self.manager_stats.copy()
        
        # Calculate average session duration from archived sessions
        if self.archived_sessions:
            durations = [
                summary['analytics']['session_duration_minutes']
                for summary in self.archived_sessions.values()
                if summary['analytics']['session_duration_minutes'] > 0
            ]
            
            if durations:
                metrics['average_session_duration'] = sum(durations) / len(durations)
        
        return metrics
    
    async def generate_session_report(
        self,
        session_id: str,
        include_full_transcript: bool = False
    ) -> Dict[str, Any]:
        """Generate comprehensive report for a session."""
        session = self.get_session(session_id)
        
        if not session and session_id in self.archived_sessions:
            # Use archived data
            summary = self.archived_sessions[session_id]
            report = {
                'session_summary': summary,
                'recommendations': [],
                'insights': []
            }
        elif session:
            # Generate report for active session
            summary = session.get_session_summary()
            
            report = {
                'session_summary': summary,
                'recommendations': self._generate_recommendations(session),
                'insights': self._generate_insights(session)
            }
            
            if include_full_transcript:
                report['full_transcript'] = session.export_conversation("json")
        else:
            raise ValueError(f"Session {session_id} not found")
        
        return report
    
    def _generate_recommendations(self, session: DialogueSession) -> List[str]:
        """Generate recommendations for improving the session."""
        recommendations = []
        
        analytics = session.analytics
        
        # Accuracy recommendations
        if analytics.average_accuracy_score < 0.8:
            recommendations.append(
                "Consider reviewing historical facts and adding more authoritative sources"
            )
        
        # Engagement recommendations
        if analytics.engagement_score < 0.6:
            recommendations.append(
                "Try to encourage more balanced participation from all historical figures"
            )
        
        # Learning objectives
        unachieved_objectives = [
            obj for obj in session.learning_objectives.values()
            if not obj.is_achieved
        ]
        
        if unachieved_objectives:
            recommendations.append(
                f"Focus on completing {len(unachieved_objectives)} remaining learning objectives"
            )
        
        return recommendations
    
    def _generate_insights(self, session: DialogueSession) -> List[str]:
        """Generate insights about the session."""
        insights = []
        
        analytics = session.analytics
        
        # Participation insights
        if analytics.messages_by_participant:
            most_active = max(
                analytics.messages_by_participant.items(),
                key=lambda x: x[1]
            )
            participant_name = session.participants[most_active[0]].name
            insights.append(f"{participant_name} was the most active participant")
        
        # Quality insights
        if analytics.average_accuracy_score > 0.9:
            insights.append("Excellent historical accuracy maintained throughout the conversation")
        
        # Cultural insights
        if analytics.average_cultural_authenticity > 0.8:
            insights.append("Strong cultural authenticity demonstrated by all participants")
        
        return insights
    
    def reset_stats(self):
        """Reset session manager statistics."""
        self.manager_stats = {
            'total_sessions_created': 0,
            'active_sessions_count': len(self.active_sessions),
            'completed_sessions_count': 0,
            'expired_sessions_count': 0,
            'average_session_duration': 0.0
        }
        logger.info("Session manager statistics reset")